﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.Threading;

namespace XamlTorrent.Managers.UriGetter.Downloaders
{
    /// <summary>
    /// Provides mechanism such as partitioning the file into segments
    /// and worker thread control. 
    /// </summary>
    abstract class DownloaderBase : IDisposable
    {
        /// <summary>
        /// Initializes the DownloaderBase, should be invoked by deriving classes.
        /// </summary>
        /// <param name="source">URI to download from.</param>
        /// <param name="dest">File where to write the data.</param>
        protected DownloaderBase(Uri source, FileInfo dest)
        {
            #region Parameter Validation
            if (source == null)
                throw new ArgumentNullException("source");

            if (dest == null)
                throw new ArgumentNullException("dest");
            #endregion

            sourceUri = source;
            destFile = dest;

            fileParts = new List<FileSegment>();
        }

        #region Protocol specific abstract methods
        /// <summary>
        /// Protocol-specific function which retrives content metadata.
        /// </summary>
        protected abstract ContentHeaderData GetHeaderData();

        /// <summary>
        /// Synchronously fills the segment with data.
        /// </summary>
        /// <param name="segment">FileSegment which the invokation is responsible for filling.</param>
        /// <param name="parallelSegment">True if there is more than 1 segment running in parallel.
        /// False if this is the only segment running.</param>
        protected abstract long DownloadSegment(FileSegment segment); 
        #endregion

        #region Properties
        /// <summary>
        /// Number of segments running in parallel.
        /// </summary>
        public int ActiveSegmentCount
        {
            get { return fileParts.Count; }
        }

        private int maxSegments = 1;
        /// <summary>
        /// Number of segments the downloader will try to use.
        /// </summary>
        public int MaxSegmentCount
        {
            get { return maxSegments; }
            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value", "MaxSegmentCount must be positive.");

                maxSegments = value;
            }
        }

        /// <summary>
        /// File stream of destFile.
        /// </summary>
        private Stream fileStream;

        /// <summary>
        /// Uri to download from.
        /// </summary>
        private Uri sourceUri;

        /// <summary>
        /// URI of the file to be downloaded.
        /// </summary>
        public Uri Source
        {
            get { return sourceUri; }
        }

        /// <summary>
        /// Local file where data will be written.
        /// </summary>
        private FileInfo destFile;

        /// <summary>
        /// Local file where downloader writes data.
        /// </summary>
        public FileInfo Destination
        {
            get { return destFile; }
        }

        /// <summary>
        /// Total length of the file.
        /// </summary>
        public long BytesTotal
        {
            get
            {
                lock (fileParts)
                {
                    long sum = 0;

                    lock (fileParts)
                    {
                        fileParts.ForEach((FileSegment seg) => sum += seg.Length);
                    }

                    return sum;
                }
            }
        }

        /// <summary>
        /// Total number of bytes downloaded by all worker threads.
        /// </summary>
        public long BytesDownloaded
        {
            get
            {
                lock (fileParts)
                {
                    long sum = 0;

                    lock (fileParts)
                    {
                        fileParts.ForEach((FileSegment seg) => sum += seg.BytesWritten);
                    }

                    return sum;
                }
            }
        } 
        #endregion

        #region Parallel segment downloading mechanism
        /// <summary>
        /// List of file segments being downloaded concurrently.
        /// </summary>
        private List<FileSegment> fileParts;

        /// <summary>
        /// Controller thread which has been launched asynchronously.
        /// </summary>
        private Thread controller = null;

        /// <summary>
        /// If set to true indicates that all segment threads should terminate
        /// as soon as possible.
        /// </summary>
        protected bool stopAllSegments = false;

        /// <summary>
        /// Invokes StartDownload() in a new thread.
        /// </summary>
        public void StartDownloadAsync()
        {
            controller = new Thread(StartDownload);
            controller.Start();
        }

        /// <summary>
        /// Starts downloading the file.
        /// </summary>
        private void StartDownload()
        {
            ContentHeaderData header = GetHeaderData();

            bool multiSegments = (header.CanSegment ?? false);
            int segmentCount = multiSegments ? maxSegments : 1;

            fileStream = destFile.Open(
                FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read
                );
            
            lock (fileParts)
            {
                PartitionFile(header.ContentLength, segmentCount);
            }

            fileStream.SetLength(header.ContentLength); // allocate the file

            // Parallel lists of file segmets and the thread running it.
            List<Thread> threads = new List<Thread>(segmentCount);
            List<FileSegment> active = new List<FileSegment>(segmentCount);
            List<FileSegment> finished = new List<FileSegment>(segmentCount);

            #region Start a thread for each segment
            foreach (FileSegment seg in fileParts)
            {
                Thread t = new Thread(StartDownloadSegment);

                t.Start(seg);

                active.Add(seg);
                threads.Add(t);
            }
            #endregion

            // Keep polling the threads until all of them finish.
            for (int i = 0; threads.Count > 0; i++)
            {
                i %= threads.Count;
                if (threads[i].Join(50))
                {
                    FileSegment seg = active[i];

                    finished.Add(seg);
                    threads.RemoveAt(i);
                    active.RemoveAt(i);

                    List<FileSegment> newSegs = FillSegmentGaps(
                        header.ContentLength, segmentCount, active, finished);

                    foreach (FileSegment newSeg in newSegs)
                    {
                        Thread t = new Thread(StartDownloadSegment);

                        t.Start(newSeg);

                        active.Add(newSeg);
                        threads.Add(t);
                    }
                }
            }

            fileStream.Close();

            // All threads done.
        }

        private List<FileSegment> FillSegmentGaps(long contentLength, int segmentCount, List<FileSegment> active, List<FileSegment> finished)
        {
            List<FileSegment> fill = new List<FileSegment>(MaxSegmentCount);

            //long pos = 0;

            //while (pos < (contentLength-1))
            //{
            //    // Find a segment such that it starts before or at pos and ends after pos.
            //    FileSegment seg = active.Find((FileSegment s) => (s.Offset <= pos && s.EndPosition > pos));

            //    if (seg != null)
            //        pos = (seg.EndPosition + 1); // go to the end of this segment
            //    else
            //    {
            //        FileSegment seg = active.Find((FileSegment s) => (s.Offset <= pos && s.EndPosition > pos));    
            //    }
            //}

            return fill;
        }

        /// <summary>
        /// Helper function which invokes DownloadSegment(...).
        /// </summary>
        /// <param name="p">Array of objects representing the parameters of DownloadSegment(FileSegment, bool).</param>
        private void StartDownloadSegment(object p)
        {
            Exception error = null;

            try
            {
                DownloadSegment((FileSegment)p);
            }
            catch (Exception ex)
            {
                error = ex;
            }
        }

        /// <summary>
        /// Returns true if the downloader supports multiple connections per file.
        /// </summary>
        public virtual bool SupportsSegmentation
        {
            get { return true; }
        }

        /// <summary>
        /// Splits the file into <param name="segmentCount" /> segments.
        /// </summary>
        /// <param name="contentLength">Size of the file in bytes.</param>
        /// <param name="segmentCount">Number of partitions to create.</param>
        private void PartitionFile(long contentLength, int segmentCount)
        {
            long pos = 0; // position up to which the segments covered
            long partLen = (contentLength / segmentCount);

            fileParts.Clear();
            fileParts.Capacity = segmentCount;

            for (int i = 0; i < segmentCount; i++)
            {
                fileParts.Add(
                    new FileSegment(fileStream, pos, partLen, contentLength)
                    );

                pos += (partLen + 1);
            }

            // Check for round-off error from partLen calculation
            long rem = (contentLength - pos + 1);
            if (rem != 0)
            {
                // Remove last element, it's improperly sized
                fileParts.RemoveAt(fileParts.Count - 1);

                // Set position to the end of the now last element.
                pos = fileParts[fileParts.Count - 1].Offset + (partLen + 1);
                // Last partition fills exactly the remaining bytes
                long lastPartLen = (contentLength - pos);

                fileParts.Add(
                    new FileSegment(fileStream, pos, lastPartLen, contentLength)
                    );
            }
        } 
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (fileStream != null)
                fileStream.Dispose();
        }

        #endregion
    }
}
